package com.ruoyi.common;


import com.ruoyi.common.domin.LockConstants;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.OutputStream;
import java.net.*;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName AsyncTcpServer
 * @Description Tcp异步通信服务端
 * @Author 张永永
 * @Date 2020-12-22 16:40
 * @Version 1.0.0.0
 **/
@Component
public class AsyncTcpServer {

    //本地IP地址（以字符串的形式表现）
    private String localAddress;
    //端口号
    private int localPort;
    //服务器使用的异步ServerSocket
    private ServerSocket serverSocket;
    //服务端运行状态，默认为未运行
    private boolean isRunning = false;
    //最大连接数
    private int connMaxNum;
    //客户端socket会话与服务端接收字节数组数据映射
    private Map<String, byte[]> addressMapBytes;
    //客户端ID地址与socket会话映射
    private Map<String, Socket> ipAddressMapSocket;
    /****************************************************************************************************
     * 创建缓存线程池，线程池中的线程数不一定，根据JDK文档的描述  线程池会根据需要创建新的线程，
     * 但是也会重用以前创建的处于空闲状态的线程，如果一个线程60秒没有被使用，将会被终止 并被从缓存区移除
     ***************************************************************************************************/
    private ExecutorService threadPool = Executors.newCachedThreadPool();

    /**
     * 客户端socket会话与服务端接收字节数组映射
     *
     * @return
     */
    public Map<String, byte[]> getAddressMapBytes() {
        return addressMapBytes;
    }

    /**
     * 获取IP地址映射SOCKET会话
     *
     * @return
     */
    public Map<String, Socket> getIpAddressMapSocket() {
        return ipAddressMapSocket;
    }


    /**
     * 初始化服务端
     *
     * @param ipAddress ip地址
     * @param port      服务端端口号
     * @param maxNum    最大连接数
     */

    public void initServer(String ipAddress, int port, int maxNum) {
        localAddress = ipAddress;
        localPort = port;
        connMaxNum = maxNum;
        addressMapBytes = new ConcurrentHashMap();
        ipAddressMapSocket = new ConcurrentHashMap<String, Socket>();
    }

    /**
     * 启动服务
     */
    public void launchServer() {
        System.out.println("launchServer启动");
        try {
            if (isRunning == false) {
                serverSocket = new ServerSocket(localPort, connMaxNum, InetAddress.getByName(localAddress));
                isRunning = true;
                //继承thread类实现多线程（匿名方法）
                new Thread() {
                    @Override
                    public void run() {
                        while (isRunning) {
                            try {
                                //监听客户端连接socket会话
                                Socket socket = serverSocket.accept();
                                //初始化监听到的socket会话
                                StartListen listen = new StartListen();
                                listen.initStartListen(socket);
                                //创建线程
                                threadPool.execute(listen);
                            } catch (Exception ex) {
                                //TODO 记录日志
                                System.out.print(ex.getMessage());

                            }
                        }
                    }
                }.start();
            }
        } catch (Exception ex) {
            System.out.print(ex.getMessage());
            //TODO 记录日志
        }
    }

    /**
     * 监听每个socket会话的数据
     */
    public class StartListen implements Runnable {
        //定义套接字对象
        Socket receiveSocket;

        /**
         * 初始化
         *
         * @param socket
         */
        public void initStartListen(Socket socket) {
            try {
                this.receiveSocket = socket;
                //定义接收数据超时时间为10秒钟
                this.receiveSocket.setSoTimeout(10000);
            } catch (Exception ex) {

            }
        }

        /**
         * 合并字节数组
         *
         * @param a
         * @param b
         * @return
         */
        public byte[] concatBytes(byte[] a, byte[] b) {
            byte[] c = new byte[a.length + b.length];
            System.arraycopy(a, 0, c, 0, a.length);
            System.arraycopy(b, 0, c, a.length, b.length);
            return c;
        }

        /**
         * 实现run方法
         */
        @Override
        public void run() {
            try {
                /***********************************************************************************************
                 * 如果在分站及交换机层次不出现粘包，根据IP层分片原理最大发送1460字节数据
                 * 分站设备尽量避免IP层分片，所以设置发送给上位机数据最大1400字节数据
                 * 上位机如果接受速度慢，操作系统层容易出现粘包
                 * 此处等上位机接收完数据后尽快处理完毕等待下一帧数据的接收
                 ***********************************************************************************************/
                while (isRunning) {//循环运行监听该会话数据
                    //Thread.sleep(1000);

                    byte[] receiveData = new byte[10240];
                    //监听客户端数据，并计算读取到的客户端数据字节长度
                    int receiveLength = receiveSocket.getInputStream().read(receiveData);
                    //获取客户端IP地址
                    String socketAddress = receiveSocket.getInetAddress().toString().replace("/", "");

                    //接收长度为0或者为-1，说明客户端已经断开连接
                    if (receiveLength == 0 || receiveLength == -1) {
                        //应该从全局socket中移除该socket会话，并且退出循环
                        addressMapBytes.remove(socketAddress);
                        ipAddressMapSocket.remove(socketAddress);

                        break;
                    } else {
                        /***********************************************************************************************
                         * 分站每次发送最多512字节，如果数据到了512字节，分站随时发送，通过检验两个连续的包时间差在
                         * 几毫秒之内，所以下一次数据需要拼接到上一包数据后面，否则如果处理端来不及处理则该数据丢失
                         ***********************************************************************************************/
                        LockConstants.PERSON_PROTOCOL_LOCK.lock();//加锁
                        try {
                            byte[] concatReceive;
                            if (addressMapBytes.containsKey(socketAddress) == true) {
                                byte[] startReceive = addressMapBytes.get(socketAddress);
                                byte[] endReceive = Arrays.copyOfRange(receiveData, 0, receiveLength);
                                concatReceive = concatBytes(startReceive, endReceive);
                            } else {
                                concatReceive = Arrays.copyOfRange(receiveData, 0, receiveLength);
                            }
                            addressMapBytes.put(socketAddress, concatReceive);
                            //将IP地址与socket会话对应
                            ipAddressMapSocket.put(socketAddress, receiveSocket);
                        } catch (Exception ex) {
                            LockConstants.PERSON_PROTOCOL_LOCK.unlock();//异常解锁
                            break;
                        }
                        LockConstants.PERSON_PROTOCOL_LOCK.unlock();//正常解锁
                    }
                }
            } catch (SocketTimeoutException ste) {
                //应该从全局socket中移除该socket会话，并且退出循环
                addressMapBytes.remove(receiveSocket.getInetAddress().toString().replace("/", ""));
            } catch (Exception ex) {
                System.out.println("客户端IP地址："+receiveSocket.getInetAddress().toString().replace("/", ""));
                //TODO 记录日志
                ex.printStackTrace();
            }
        }
    }

    /**
     * 通过socket会话发送数据
     *
     * @param socket      socket会话连接
     * @param sendMessage 发送的数据
     */
    public static String sendData(Socket socket, byte[] sendMessage) {
        //判断是否为空
        try {
            OutputStream socketOut = socket.getOutputStream();
            socketOut.write(sendMessage);
            socketOut.flush();
            return "1";
        } catch (Exception ex) {
            //TODO 记录日志
            return "0";
        }
    }
}
